Skip to main content

Classes

JavaScript classes in Flow operate both as a value and a type. You can use the name of the class as the type of its instances:

1class MyClass {2  // ...3}4
5const myInstance: MyClass = new MyClass(); // Works!

This is because classes in Flow are nominally typed.

This means two classes with identical shapes are not compatible:

1class A {2  x: number;3}4class B {5  x: number;6}7const foo: B = new A(); // Error!
8const bar: A = new B(); // Error!
7:16-7:22: Cannot assign `new A()` to `foo` because `A` [1] is incompatible with `B` [2]. [incompatible-type]
8:16-8:22: Cannot assign `new B()` to `bar` because `B` [1] is incompatible with `A` [2]. [incompatible-type]

You also cannot use an object type to describe an instance of a class:

1class MyClass {2  x: number;3}4const foo: {x: number, ...} = new MyClass(); // Error!
4:31-4:43: Cannot assign `new MyClass()` to `foo` because `MyClass` [1] is not a subtype of object type [2]. Class instances are not subtypes of object types; consider rewriting object type [2] as an interface. [class-object-subtyping]

You can use interfaces to accomplish this instead:

1class A {2  x: number;3}4class B {5  x: number;6}7
8interface WithXNum {9  x: number;10}11
12const foo: WithXNum = new A(); // Works!13const bar: WithXNum = new B(); // Works!14
15const n: number = foo.x; // Works!

Class Syntax

Classes in Flow are just like normal JavaScript classes, but with added types.

Class Methods

Just like in functions, class methods can have annotations for both parameters (input) and returns (output):

1class MyClass {2  method(value: string): number {3    return 0;4  }5}

Also just like regular functions, class methods may have this annotations as well. However, if one is not provided, Flow will infer the class instance type (or the class type for static methods) instead of mixed. When an explicit this parameter is provided, it must be a supertype of the class instance type (or class type for static methods).

1class MyClass {2  method(this: interface {x: string}) { /* ... */ } // Error!
3}
2:3-2:8: Cannot define method `method` [1] on `MyClass` because property `x` is missing in `MyClass` [2] but exists in interface type [3]. [prop-missing]

Unlike class properties, however, class methods cannot be unbound or rebound from the class on which you defined them. So all of the following are errors in Flow:

1class MyClass { method() {} }2const a = new MyClass();3a.method; // Error!
4const {method} = a; // Error!
5a.method.bind({}); // Error!
3:3-3:8: Cannot get `a.method` because property `method` [1] cannot be unbound from the context [2] where it was defined. [method-unbinding]
4:8-4:13: property `method` [1] cannot be unbound from the context [2] where it was defined. [method-unbinding]
5:3-5:8: Cannot get `a.method` because property `method` [1] cannot be unbound from the context [2] where it was defined. [method-unbinding]

Methods are considered read-only:

1class MyClass {2  method() {}3}4
5const a = new MyClass();6a.method = function() {}; // Error!
6:3-6:8: Cannot assign function to `a.method` because property `method` is not writable. [cannot-write]

Flow supports private methods, a feature of ES2022. Private methods start with a hash symbol #:

1class MyClass {2  #internalMethod() {3    return 1;4  }5  publicApi() {6    return this.#internalMethod();7  }8}9
10const a = new MyClass();11a.#internalMethod(); // Error!
12a.publicApi(); // Works!
11:3-11:17: Private fields can only be referenced from within a class.

Flow requires return type annotations on methods in most cases. This is because it is common to reference this inside of a method, and this is typed as the instance of the class - but to know the type of the class we need to know the return type of its methods!

1class MyClass {2  foo() { // Error!
3 return this.bar();4 }5 bar() { // Error!
6 return 1;7 }8}
2:8-2:7: Missing an annotation on return. [missing-local-annot]
5:8-5:7: Missing an annotation on return. [missing-local-annot]
1class MyClassFixed {2  foo(): number { // Works!3    return this.bar();4  }5  bar(): number { // Works!6    return 1;7  }8}

Class Fields (Properties)

Whenever you want to use a class field in Flow you must first give it an annotation:

1class MyClass {2  method() {3    this.prop = 42; // Error!
4 }5}
3:10-3:13: Cannot assign `42` to `this.prop` because property `prop` is missing in `MyClass` [1]. [prop-missing]

Fields are annotated within the body of the class with the field name followed by a colon : and the type:

1class MyClass {2  prop: number;3  method() {4    this.prop = 42;5  }6}

Fields added outside of the class definition need to be annotated within the body of the class:

1function func(x: number): number {2  return x + 1;3}4
5class MyClass {6  static constant: number;7  static helper: (number) => number;8  prop: number => number;9}10MyClass.helper = func11MyClass.constant = 4212MyClass.prototype.prop = func

Flow also supports using the class properties syntax:

1class MyClass {2  prop = 42;3}

When using this syntax, you are not required to give it a type annotation. But you still can if you need to:

1class MyClass {2  prop: number = 42;3}

You can mark a class field as read-only (or write-only) using variance annotations. These can only be written to in the constructor:

1class MyClass {2  +prop: number;3
4  constructor() {5    this.prop = 1; // Works!6  }7
8  method() {9    this.prop = 1; // Error!
10 }11}12 13const a = new MyClass();14const n: number = a.prop; // Works!15a.prop = 1; // Error!
9:10-9:13: Cannot assign `1` to `this.prop` because property `prop` is not writable. [cannot-write]
15:3-15:6: Cannot assign `1` to `a.prop` because property `prop` is not writable. [cannot-write]

Flow supports private fields, a feature of ES2022. Private fields start with a hash symbol #:

1class MyClass {2  #internalValue: number;3
4  constructor() {5    this.#internalValue = 1;6  }7
8  publicApi() {9    return this.#internalValue;10  }11}12
13const a = new MyClass();14const x: number = a.#internalValue; // Error!
15const y: number = a.publicApi(); // Works!
14:21-14:34: Private fields can only be referenced from within a class.

Extending classes and implementing interfaces

You can optionally extend one other class:

1class Base {2  x: number;3}4
5class MyClass extends Base {6  y: string;7}

And also implement multiple interfaces:

1interface WithXNum {2  x: number;3}4interface Readable {5  read(): string;6}7
8class MyClass implements WithXNum, Readable {9  x: number;10  read(): string {11    return String(this.x);12  }13}

You don't need to implement an interface to be a subtype of it, but doing so enforces that your class meets the requirements:

1interface WithXNum {2  x: number;3}4
5class MyClass implements WithXNum { // Error!
6}
5:7-5:13: Cannot implement `WithXNum` [1] with `MyClass` because property `x` is missing in `MyClass` [2] but exists in `WithXNum` [1]. [prop-missing]

Class Constructors

You can initialize your class properties in class constructors:

1class MyClass {2  foo: number;3
4  constructor() {5    this.foo = 1;6  }7}

You must first call super(...) in a derived class before you can access this and super:

1class Base {2  bar: number;3}4
5class MyClass extends Base {6  foo: number;7
8  constructor() {9    this.foo; // Error
10 this.bar; // Error
11 super.bar; // Error
12 super();13 this.foo; // OK14 this.bar; // OK15 super.bar; // OK16 }17}
9:5-9:8: Must call `super` before accessing this [1] in a derived constructor. [reference-before-declaration]
10:5-10:8: Must call `super` before accessing this [1] in a derived constructor. [reference-before-declaration]
11:5-11:9: Must call `super` before accessing super [1] in a derived constructor. [reference-before-declaration]

However, Flow will not enforce that all class properties are initialized in constructors:

1class MyClass {2  foo: number;3  bar: number;4
5  constructor() {6    this.foo = 1;7  }8
9  useBar() {10    (this.bar: number); // No errors.11  }12}

Class Generics

Classes can also have their own generics:

1class MyClass<A, B, C> {2  property: A;3  method(val: B): C {4    throw new Error();5  }6}

Class generics are parameterized. When you use a class as a type you need to pass parameters for each of its generics:

1class MyClass<A, B, C> {2  constructor(arg1: A, arg2: B, arg3: C) {3    // ...4  }5}6
7const val: MyClass<number, boolean, string> = new MyClass(1, true, 'three');

Classes in annotations

When you use the name of your class in an annotation, it means an instance of your class:

class MyClass {}

const b: MyClass = new MyClass(); // Works!
const a: MyClass = MyClass; // Error!

See here for details on Class<T>, which allows you to refer to the type of the class in an annotation.